/*
 * Copyright 2011-2025 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0.
 * See `LICENSE` in the project root for license information.
 */

package me.ijleex.platform.framework.minio.util;

import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.web.multipart.MultipartFile;

import io.minio.BucketExistsArgs;
import io.minio.DownloadObjectArgs;
import io.minio.GetObjectArgs;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.ListObjectsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.ObjectWriteResponse;
import io.minio.PutObjectArgs;
import io.minio.RemoveBucketArgs;
import io.minio.RemoveObjectArgs;
import io.minio.RemoveObjectsArgs;
import io.minio.Result;
import io.minio.StatObjectArgs;
import io.minio.StatObjectResponse;
import io.minio.UploadObjectArgs;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;

/**
 * MinIO操作工具类.
 *
 * <p>使用{@link MinioClient}操作MinIO。</p>
 *
 * @author liym
 * @see #setMinioClient(MinioClient)
 * @see #setupClient(String, String, String)
 * @since 2024-12-31 22:00 新建
 */
public final class MinioUtils {

    /**
     * MinIO对象文件访问地址有效时间（以小时单位，默认是7天）
     */
    private static final int DEFAULT_EXPIRY_TIME = 7 * 24 * 3600;

    private static final Logger logger = LoggerFactory.getLogger(MinioUtils.class);

    private MinioUtils() {
        throw new IllegalAccessError("Utility class");
    }

    /**
     * 设置{@link MinioClient MinIO客户端}.
     *
     * @param minioClient MinIO客户端
     */
    public static void setMinioClient(final MinioClient minioClient) {
        Assert.notNull(minioClient, () -> "'MinioClient' must not be null");
        Lazy.minioClient = minioClient;
        logger.info("Set 'MinioClient' done");
    }

    /**
     * 设置客户端连接属性，自动创建连接，设置{@link MinioClient MinIO客户端}.
     *
     * @param endpoint 客户端连接地址，如{@code http://127.0.0.1:9000}
     * @param accessKey 用户名
     * @param secretKey 密码
     * @since 2025-06-21 17:00
     */
    public static void setupClient(String endpoint, String accessKey, String secretKey) {
        if (StringUtils.isAnyBlank(endpoint, accessKey, secretKey)) {
            logger.warn("MinIO连接属性不正确：endpoint={}, accessKey={}, secretKey={}",
                    endpoint, accessKey, secretKey);
            return;
        }

        MinioClient minioClient = MinioClient.builder()
                // https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html#API_CreateBucket_RequestSyntax
                .region("cn-north-1")
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
        setMinioClient(minioClient);
    }

    // ===

    /**
     * 判断桶是否存在
     *
     * @param bucketName 桶名称
     * @return true/false
     */
    public static boolean bucketExists(String bucketName) {
        boolean exists = false;
        try {
            exists = Lazy.minioClient().bucketExists(BucketExistsArgs.builder()
                    .bucket(bucketName)
                    .build());
        } catch (Exception e) {
            logger.error("bucketExists error; bucketName={}", bucketName, e);
        }
        return exists;
    }

    /**
     * 创建桶
     *
     * @param bucketName 桶名称
     * @return true/false
     */
    public static boolean makeBucket(String bucketName) {
        try {
            boolean exists = bucketExists(bucketName);
            if (exists) {
                return true;
            }
            Lazy.minioClient().makeBucket(MakeBucketArgs.builder()
                    .bucket(bucketName)
                    .objectLock(false)
                    .build());
            return true;
        } catch (Exception e) {
            logger.error("makeBucket error; bucketName={}", bucketName, e);
            return false;
        }
    }

    /**
     * 移除桶
     *
     * @param bucketName 桶名称
     * @return true/false
     */
    public static boolean removeBucket(String bucketName) {
        try {
            boolean exists = bucketExists(bucketName);
            if (!exists) {
                return true;
            }
            // 删除存储桶，注意，只有存储桶为空时才能删除成功。
            Lazy.minioClient().removeBucket(RemoveBucketArgs.builder()
                    .bucket(bucketName)
                    .build());
            return true;
        } catch (Exception e) {
            logger.error("removeBucket error; bucketName={}", bucketName, e);
            return false;
        }
    }

    /**
     * 列出桶名称
     *
     * @return 桶名称列表
     */
    public static List<String> listBuckets() {
        List<String> bucketList;
        try {
            List<Bucket> buckets = Lazy.minioClient().listBuckets();
            bucketList = new ArrayList<>(buckets.size());
            for (Bucket bucket : buckets) {
                bucketList.add(bucket.name());
            }
        } catch (Exception e) {
            logger.error("listBuckets error", e);
            bucketList = List.of();
        }
        return bucketList;
    }

    /**
     * 列出桶下的对象
     *
     * @param bucketName 桶名称
     * @return 对象列表
     */
    public static Iterable<Result<Item>> listObjects(String bucketName) {
        return Lazy.minioClient().listObjects(ListObjectsArgs.builder()
                .bucket(bucketName)
                .build());
    }

    /**
     * 列出桶下的对象名称
     *
     * @param bucketName 桶名称
     * @return 对象列表
     */
    public static List<String> listObjectNames(String bucketName) {
        List<String> objectList = new ArrayList<>();
        try {
            boolean exists = bucketExists(bucketName);
            if (exists) {
                Iterable<Result<Item>> objects = listObjects(bucketName);
                for (Result<Item> result : objects) {
                    Item item = result.get();
                    objectList.add(item.objectName());
                }
            }
        } catch (Exception e) {
            logger.error("listObjectNames error; bucketName={}", bucketName, e);
        }
        return objectList;
    }

    /**
     * 上传对象（文件）
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @param file 文件对象
     * @return true/false
     */
    public static boolean uploadObject(String bucketName, String objectName, MultipartFile file) {
        makeBucket(bucketName);
        try (InputStream in = file.getInputStream()) {
            ObjectWriteResponse putRsp = Lazy.minioClient().putObject(PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .stream(in, file.getSize(), -1L)
                    .contentType(file.getContentType())
                    .build());
            String etag = putRsp.etag();
            logger.info("uploadObject: bucketName={}, objectName={}, etag={}", bucketName, objectName, etag);
            return true;
        } catch (Exception e) {
            logger.error("uploadObject error; bucketName={}, objectName={}", bucketName, objectName, e);
            return false;
        }
    }

    /**
     * 上传对象（文件）
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @param in 文件流
     * @param contentType 文件类型
     * @return true/false
     */
    public static boolean uploadObject(String bucketName, String objectName, InputStream in, String contentType) {
        makeBucket(bucketName);
        try {
            ObjectWriteResponse putRsp = Lazy.minioClient().putObject(PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    // 不清楚文件的大小时，可以传-1，10485760，如果知道大小则可以传入objectSize，partSize
                    .stream(in, -1L, 10485760L)
                    .contentType(contentType)
                    .build());
            String etag = putRsp.etag();
            logger.info("uploadObject: bucketName={}, objectName={}, etag={}", bucketName, objectName, etag);
            return true;
        } catch (Exception e) {
            logger.error("uploadObject error; bucketName={}, objectName={}", bucketName, objectName, e);
            return false;
        }
    }

    /**
     * 上传对象（文件）
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @param file 文件对象
     * @return true/false
     */
    public static boolean uploadObject(String bucketName, String objectName, Path file) {
        makeBucket(bucketName);
        try {
            ObjectWriteResponse uploadRsp = Lazy.minioClient().uploadObject(UploadObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .filename(file.toString())
                    .contentType(Files.probeContentType(file))
                    .build());
            String etag = uploadRsp.etag();
            logger.info("uploadObject: bucketName={}, objectName={}, etag={}", bucketName, objectName, etag);
            return true;
        } catch (Exception e) {
            logger.error("uploadObject error; bucketName={}, objectName={}", bucketName, objectName, e);
            return false;
        }
    }

    /**
     * 上传对象（文件）
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @param file 文件对象
     * @return true/false
     */
    public static boolean uploadObject(String bucketName, String objectName, File file) {
        return uploadObject(bucketName, objectName, file.toPath());
    }

    /**
     * 获取对象的元数据
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @return 对象元数据
     */
    public static StatObjectResponse statObject(String bucketName, String objectName) {
        StatObjectResponse statObject = null;
        try {
            statObject = Lazy.minioClient().statObject(StatObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .build());
        } catch (Exception e) {
            logger.error("statObject error; bucketName={}, objectName={}", bucketName, objectName, e);
        }
        return statObject;
    }

    /**
     * 获取文件访问地址
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @return 文件访问地址
     */
    public static String getObjectUrl(String bucketName, String objectName) {
        try {
            return Lazy.minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(bucketName)
                    .object(objectName)
                    .build());
        } catch (Exception e) {
            logger.error("getObjectUrl error; bucketName={}, objectName={}", bucketName, objectName, e);
            return "";
        }
    }

    /**
     * 获取文件访问地址，并设置地址失效时间
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @param expires 失效时间（以小时单位），默认是7天，不得大于7天
     * @return 文件访问地址
     */
    public static String getObjectUrl(String bucketName, String objectName, int expires) {
        String url = "";
        try {
            if (expires < 1 || expires > DEFAULT_EXPIRY_TIME) {
                throw new IllegalArgumentException("Expires must be in range of 1 to " + DEFAULT_EXPIRY_TIME);
            }
            url = Lazy.minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(bucketName)
                    .object(objectName)
                    .expiry(expires, TimeUnit.HOURS) // 动态参数
                    // .expiry(24 * 60 * 60) // 用秒来计算一天时间有效期
                    // .expiry(1, TimeUnit.DAYS) // 按天传参
                    // .expiry(1, TimeUnit.HOURS) // 按小时传参数
                    .build());
        } catch (Exception e) {
            logger.error("getObjectUrl error; bucketName={}, objectName={}", bucketName, objectName, e);
        }
        return url;
    }

    /**
     * 下载文件，并将文件保存到本地
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @param fileName 保存本地文件
     * @return true/false
     */
    public static boolean downloadObject(String bucketName, String objectName, String fileName) {
        try {
            StatObjectResponse statObject = statObject(bucketName, objectName);
            if (statObject == null || statObject.size() == 0L) {
                return false;
            }
            Lazy.minioClient().downloadObject(DownloadObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .filename(fileName)
                    .overwrite(true) // 覆盖已存在的文件
                    .build());
            return true;
        } catch (Exception e) {
            logger.error("downloadObject error; bucketName={}, objectName={}", bucketName, objectName, e);
            return false;
        }
    }

    /**
     * 以流的形式获取一个文件对象
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @return 文件流
     */
    public static InputStream getObject(String bucketName, String objectName) {
        StatObjectResponse statObject = statObject(bucketName, objectName);
        if (statObject == null || statObject.size() == 0L) {
            return null;
        }
        try {
            return Lazy.minioClient().getObject(GetObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .build());
        } catch (Exception e) {
            logger.error("getObject error; bucketName={}, objectName={}", bucketName, objectName, e);
            return null;
        }
    }

    /**
     * 以流的形式获取一个文件对象（断点下载）
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @param offset 起始字节的位置
     * @param length 要读取的长度（可选，如果无值则代表读到文件结尾）
     * @return 文件流
     */
    public static InputStream getObject(String bucketName, String objectName, long offset, Long length) {
        StatObjectResponse statObject = statObject(bucketName, objectName);
        if (statObject == null || statObject.size() == 0L) {
            return null;
        }
        try {
            return Lazy.minioClient().getObject(GetObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .offset(offset)
                    .length(length)
                    .build());
        } catch (Exception e) {
            logger.error("getObject error; bucketName={}, objectName={}, offset={}, length={}",
                    bucketName, objectName, offset, length, e);
            return null;
        }
    }

    /**
     * 移除对象
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @return true/false
     */
    public boolean removeObject(String bucketName, String objectName) {
        try {
            Lazy.minioClient().removeObject(RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .build());
            return true;
        } catch (Exception e) {
            logger.error("removeObject error; bucketName={}, objectName={}", bucketName, objectName, e);
            return false;
        }
    }

    /**
     * 删除指定桶的多个文件对象，返回删除错误的对象列表，全部删除成功，返回空列表
     *
     * @param bucketName 存储桶名称
     * @param objectNames 含有要删除的多个object名称的迭代器对象 eg:
     * <pre> {@code
     * List<DeleteObject> objects = new LinkedList<>();
     * objects.add(new DeleteObject("my-objectname1"));
     * objects.add(new DeleteObject("my-objectname2"));
     * objects.add(new DeleteObject("my-objectname3"));
     * }
     * </pre>
     * @return 如果有值，说明当前文件删除失败
     */
    private static List<String> removeObjects0(String bucketName, List<DeleteObject> objectNames) {
        List<String> deleteErrorNames = new ArrayList<>();
        try {
            Iterable<Result<DeleteError>> errResults = Lazy.minioClient().removeObjects(RemoveObjectsArgs.builder()
                    .bucket(bucketName)
                    .objects(objectNames)
                    .build());
            for (Result<DeleteError> result : errResults) {
                DeleteError error = result.get();
                deleteErrorNames.add(error.objectName());
            }
        } catch (Exception e) {
            logger.error("removeObjects error; bucketName={}, objectNames={}", bucketName, objectNames, e);
        }
        return deleteErrorNames;
    }

    /**
     * 删除指定桶的多个文件对象，返回删除错误的对象列表，全部删除成功，返回空列表
     *
     * @param bucketName 存储桶名称
     * @param objectNames 含有要删除的多个object名称的迭代器对象
     * @return 如果有值，说明当前文件删除失败
     */
    public static List<String> removeObjects(String bucketName, List<String> objectNames) {
        List<DeleteObject> deleteObjects = new ArrayList<>(objectNames.size());
        objectNames.forEach(objectName -> deleteObjects.add(new DeleteObject(objectName)));
        return removeObjects0(bucketName, deleteObjects);
    }

    // ===

    /**
     * 懒加载实现单例模式.
     */
    private static class Lazy {

        private static MinioClient minioClient;

        private Lazy() {
            throw new IllegalAccessError("No instance");
        }

        /**
         * 获取{@link MinioClient MinIO客户端}
         */
        private static MinioClient minioClient() {
            Assert.state(minioClient != null, () -> "'MinioClient' has not been initialized");
            return minioClient;
        }

    }

}
